using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Phoenix.Commands;
using Phoenix.Diagnostics;
using Phoenix.Framework;

namespace Phoenix.Frames
{
    public class InvokeActionBuilder
    {
        readonly ControllerActionContext context;
        readonly ViewModelBase viewModel;

        public InvokeActionBuilder(ViewModelBase viewModel)
        {
            this.viewModel = viewModel;
        }

        public InvokeActionBuilder(ControllerActionContext context)
        {
            this.context = context;
        }

        public virtual Task InvokeAction(ControllerAction controllerAction, object[] parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            var controllerActionParameters = GetControllerActionParameters(parameters);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options);
            return ActionInvoker.InvokeAction(invokeActionRequest);
        }

        public virtual Task InvokeAction(IContentFrame targetFrame, ControllerAction controllerAction, object[] parameters = null, 
            InvokeOptions options = InvokeOptions.None)
        {
            var controllerActionParameters = GetControllerActionParameters(parameters);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options, targetFrame);
            return ActionInvoker.InvokeAction(invokeActionRequest);
        }

        public virtual AsyncDelegateCommand InvokeActionCommand(ControllerAction controllerAction, object[] parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            return new AsyncDelegateCommand(()=>InvokeAction(controllerAction, parameters, options));
        }

        public virtual AsyncDelegateCommand InvokeActionCommand(IContentFrame targetFrame, ControllerAction controllerAction, object[] parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            return new AsyncDelegateCommand(() => InvokeAction(targetFrame, controllerAction, parameters, options));
        }

        public virtual AsyncDelegateCommand<TArg> InvokeActionCommand<TArg>(ControllerAction controllerAction, Func<TArg, object[]> parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            return new AsyncDelegateCommand<TArg>(arg => InvokeAction(controllerAction, GetParameters(parameters, arg), options));
        }

        public virtual AsyncDelegateCommand<TArg> InvokeActionCommand<TArg>(IContentFrame targetFrame, ControllerAction controllerAction, Func<TArg, object[]> parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            return new AsyncDelegateCommand<TArg>(arg => InvokeAction(targetFrame, controllerAction, GetParameters(parameters, arg), options));
        }

        static object[] GetParameters<TArg>(Func<TArg, object[]> parameters, TArg arg)
        {
            if (parameters == null)
                return new object[0];

            return parameters(arg);
        }

        public virtual Task<TResult> InvokeActionWithResult<TResult>(ControllerAction controllerAction, 
            object[] parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            var controllerActionParameters = GetControllerActionParameters(parameters);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options);
            return ActionInvoker.InvokeActionWithResult<TResult>(invokeActionRequest);
        }

        public virtual Task<TResult> InvokeActionWithResult<TResult>(IContentFrame targetFrame, ControllerAction controllerAction,
            object[] parameters = null,
            InvokeOptions options = InvokeOptions.None)
        {
            var controllerActionParameters = GetControllerActionParameters(parameters);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options, targetFrame);
            return ActionInvoker.InvokeActionWithResult<TResult>(invokeActionRequest);
        }

        protected internal Task InvokeExpression(string controllerName, MethodCallExpression expressionBody, string assembly,
            InvokeOptions options = InvokeOptions.None)
        {
            ControllerActionExpressionParser parser;
            using (TraceScope.Create(this, TraceLevel.Debug, () => "Parsing Navigation Expression"))
                parser = new ControllerActionExpressionParser(expressionBody);

            var controllerActionParameters = parser.GetNavigationParameters();
            var controllerAction = new ControllerAction(controllerName, parser.ActionName, assembly);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options);
            return ActionInvoker.InvokeAction(invokeActionRequest);
        }

        protected internal Task InvokeExpression(IContentFrame targetFrame, string controllerName, MethodCallExpression expressionBody, string assembly,
            InvokeOptions options = InvokeOptions.None)
        {
            ControllerActionExpressionParser parser;
            using (TraceScope.Create(this, TraceLevel.Debug, () => "Parsing Navigation Expression"))
                parser = new ControllerActionExpressionParser(expressionBody);

            var controllerActionParameters = parser.GetNavigationParameters();
            var controllerAction = new ControllerAction(controllerName, parser.ActionName, assembly);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options, targetFrame);
            return ActionInvoker.InvokeAction(invokeActionRequest);
        }

        protected internal Task<TResult> InvokeExpressionWithResult<TResult>(string controllerName, MethodCallExpression expressionBody, string assembly,
            InvokeOptions options = InvokeOptions.None)
        {
            ControllerActionExpressionParser parser;
            using (TraceScope.Create(this, TraceLevel.Debug, () => "Parsing Navigation Expression"))
                parser = new ControllerActionExpressionParser(expressionBody);

            var controllerActionParameters = parser.GetNavigationParameters();
            var controllerAction = new ControllerAction(controllerName, parser.ActionName, assembly);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options);
            return ActionInvoker.InvokeActionWithResult<TResult>(invokeActionRequest);
        }

        protected internal Task<TResult> InvokeExpressionWithResult<TResult>(IContentFrame targetFrame, string controllerName,
            MethodCallExpression expressionBody, string assembly,
            InvokeOptions options = InvokeOptions.None)
        {
            ControllerActionExpressionParser parser;
            using (TraceScope.Create(this, TraceLevel.Debug, () => "Parsing Navigation Expression"))
                parser = new ControllerActionExpressionParser(expressionBody);

            var controllerActionParameters = parser.GetNavigationParameters();
            var controllerAction = new ControllerAction(controllerName, parser.ActionName, assembly);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options, targetFrame);
            return ActionInvoker.InvokeActionWithResult<TResult>(invokeActionRequest);
        }

        protected internal Task InvokeExpression<TArg>(string controllerName, MethodCallExpression expressionBody, TArg arg, string assembly,
            InvokeOptions options = InvokeOptions.None)
        {
            ControllerActionExpressionParser parser;
            using (TraceScope.Create(this, TraceLevel.Debug, () => "Parsing Navigation Expression"))
                parser = new ControllerActionExpressionParser(expressionBody, arg);

            var controllerActionParameters = parser.GetNavigationParameters();
            var controllerAction = new ControllerAction(controllerName, parser.ActionName, assembly);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options);
            return ActionInvoker.InvokeAction(invokeActionRequest);
        }

        protected internal Task InvokeExpression<TArg>(IContentFrame targetFrame, string controllerName, 
            MethodCallExpression expressionBody, TArg arg, string assembly,
            InvokeOptions options = InvokeOptions.None)
        {
            ControllerActionExpressionParser parser;
            using (TraceScope.Create(this, TraceLevel.Debug, () => "Parsing Navigation Expression"))
                parser = new ControllerActionExpressionParser(expressionBody, arg);

            var controllerActionParameters = parser.GetNavigationParameters();
            var controllerAction = new ControllerAction(controllerName, parser.ActionName, assembly);
            var invokeActionRequest = new InvokeActionRequest(GetContext(), controllerAction, controllerActionParameters, options, targetFrame);
            return ActionInvoker.InvokeAction(invokeActionRequest);
        }

        static IEnumerable<ControllerActionParameter> GetControllerActionParameters(IEnumerable<object> parameters)
        {
            return parameters != null
                       ? parameters.Select(ControllerActionParameter.FromObject)
                       : Enumerable.Empty<ControllerActionParameter>();
        }

        protected IActionInvoker ActionInvoker
        {
            get { return GetContext().Host.ActionInvoker; }
        }

        ControllerActionContext GetContext()
        {
            if (context != null)
            {
                return context;
            }

            if (viewModel != null)
            {
                if (viewModel.Context == null)
                {
                    const string message = @"ViewModel's Context is null, this can be caused by:

 - Invoking a controller action in a viewmodels constructor
 - Instantiating a viewmodel without calling RegisterChildViewModel";
                    throw new InvalidOperationException(message);
                }

                return viewModel.Context;
            }

            throw new InvalidOperationException("Unable to resolve ControllerActionContext. Please report this issue with reproduction steps!");
        }

        protected internal MethodCallExpression GetExpressionFrom(LambdaExpression actionSelector)
        {
            if (actionSelector == null) throw new ArgumentNullException("actionSelector");

            var body = actionSelector.Body as MethodCallExpression;
            if (body == null)
                throw new Exception("The lambda expression used for navigation could not be parsed. The lambda should be a MethodCallExpression, for example: 'x => x.Search(Text)'.");

            return body;
        }
    }
}